package dovs;

import dovs.node.*;
import dovs.analysis.*;
import dovs.ClassFileParser.PrivateException;

import java.util.*;
import java.util.zip.*;
import java.io.*;

/**
 * An interface to the class library. This class generates class and interface
 * declaration nodes from class files in the classpath.
 * <p>
 * Classes and interfaces loaded from class files will contain the declarations
 * of all declared public or protected fields, constructors methods. The fields
 * do not have initializers, and the methods and constructors do not have
 * bodies. All types will be linked to their declarations, which are loaded
 * transitively as necessary.
 * <p>
 * For efficiency, this class contains a cache of loaded class files. If a class
 * environment is initialized with the same classpath as the previous one, all
 * generated library type declarations are reused.
 */
public class ClassEnvironment {
	private static String active_classpath;

	private static Map<String, PTypeDecl> class_cache;

	private static Set<String> negative_class_cache;

	private static Set<String> package_cache;

	private static Set<String> negative_package_cache;

	private static Map<String, PTypeDecl> private_class_cache;

	/** Not to be instantiated */
	private ClassEnvironment() {
	}

	private static class ResolveTypes extends DepthFirstAdapter {
		public @Override
		void inANamedType(ANamedType type) {
			AQualifiedName name = (AQualifiedName) type.getName();
			String typename = name.nameText();
			type.decl = lookupNamedTypeInternal(typename);
			if (type.decl == null) {
				PTypeDecl typed = type.getAncestor(PTypeDecl.class);
				Errors.fatalErrorMessage(ErrorType.UNRESOLVED_TYPE_ERROR,
						"Could not find type " + typename + " (referred from "
								+ typed.canonical_name + ")");
			}
		}
	}

	/**
	 * Initiallize the class environment with the given classpath.
	 * 
	 * @param classpath
	 *            the path used to look for class files
	 */
	public static void init(String classpath) {
		if (active_classpath == null || !active_classpath.equals(classpath)) {
			active_classpath = classpath;
			class_cache = new TreeMap<String, PTypeDecl>();
			negative_class_cache = new TreeSet<String>();
			package_cache = new TreeSet<String>();
			negative_package_cache = new TreeSet<String>();
			private_class_cache = new TreeMap<String, PTypeDecl>();
		}
	}

	/**
	 * Find a class or interface in the class environment by its canonical name.
	 * 
	 * @param full_name
	 *            the canonical name of the class or interface
	 * @return the declaration of the class or interface, or <code>null</code>
	 *         is it could not be found
	 */
	public static PTypeDecl lookupNamedType(String full_name) {
		if (class_cache.containsKey(full_name)) {
			return class_cache.get(full_name);
		}
		if (private_class_cache.containsKey(full_name)) {
			return null;
		}
		if (negative_class_cache.contains(full_name)) {
			return null;
		}
		PTypeDecl decl = null;
		try {
			decl = loadClassFile(full_name);
		} catch (PrivateException e) {

		}
		if (decl == null) {
			negative_class_cache.add(full_name);
			return null;
		}

		class_cache.put(full_name, decl);

		// Make type
		decl.canonical_name = full_name;
		decl.type = Util.makeType(full_name, false);
		decl.type.decl = decl;

		decl.apply(new ResolveTypes());
		return decl;
	}

	protected static PTypeDecl lookupNamedTypeInternal(String full_name) {
		if (class_cache.containsKey(full_name)) {
			return class_cache.get(full_name);
		}
		if (private_class_cache.containsKey(full_name)) {
			return private_class_cache.get(full_name);
		}
		if (negative_class_cache.contains(full_name)) {
			return null;
		}
		PTypeDecl decl = null;
		try {
			decl = loadClassFile(full_name);

			if (decl == null) {
				negative_class_cache.add(full_name);
				return null;
			}

			class_cache.put(full_name, decl);
		} catch (PrivateException e) {
			decl = e.getTypeDecl();

			if (decl == null) {
				negative_class_cache.add(full_name);
				return null;
			}

			private_class_cache.put(full_name, decl);
		}

		// Make type
		decl.canonical_name = full_name;
		decl.type = Util.makeType(full_name, false);
		decl.type.decl = decl;

		decl.apply(new ResolveTypes());
		return decl;
	}

	private static PTypeDecl loadClassFile(String full_name)
			throws PrivateException {
		String full_name_slashed = full_name.replace('.', '/') + ".class";
		String java13_home = System.getProperty("JAVA13_HOME");
		if (java13_home == null) {
			System.err.println("JAVA13_HOME is not set");
		}
		String full_classpath = (active_classpath == null ? ""
				: active_classpath + File.pathSeparator)
				+ new File(java13_home, "jre/lib/rt.jar").getPath();
		StringTokenizer st = new StringTokenizer(full_classpath,
				File.pathSeparator, false);
		while (st.hasMoreTokens()) {
			String cp_entry = st.nextToken();
			ClassFileParser cfp;
			try {
				if (cp_entry.endsWith(".jar")) {
					cfp = new ClassFileParser(cp_entry, full_name_slashed);
				} else {
					cfp = new ClassFileParser(new File(cp_entry,
							full_name_slashed).getPath());
				}
				return cfp.makeTypeDecl();
			} catch (NullPointerException e) {
			} catch (IOException e) {
			}
		}
		return null;
	}

	public static boolean packageExists(String package_name) {
		if (package_cache.contains(package_name)) {
			return true;
		}
		if (negative_package_cache.contains(package_name)) {
			return false;
		}
		boolean result = packageExistsInternal(package_name);
		if (result) {
			package_cache.add(package_name);
		} else {
			negative_package_cache.add(package_name);
		}
		return result;
	}

	private static boolean packageExistsInternal(String package_name) {
		String package_name_slashed = package_name.replace('.', '/');
		String java13_home = System.getProperty("JAVA13_HOME");
		String full_classpath = (active_classpath == null ? ""
				: active_classpath + File.pathSeparator)
				+ new File(java13_home, "jre/lib/rt.jar").getPath();
		StringTokenizer st = new StringTokenizer(full_classpath,
				File.pathSeparator, false);
		while (st.hasMoreTokens()) {
			String cp_entry = st.nextToken();
			try {
				if (cp_entry.endsWith(".jar")) {
					ZipFile zf = new ZipFile(cp_entry);
					Enumeration<? extends ZipEntry> entries = zf.entries();
					while (entries.hasMoreElements()) {
						ZipEntry ze = entries.nextElement();
						if (ze.getName().startsWith(package_name_slashed)
								&& (ze.getName().length() > package_name_slashed.length() && ze.getName().charAt(
										package_name_slashed.length()) == '/')
								&& ze.getName().endsWith(".class")) {
							return true;
						}
					}
					zf.close();
				} else {
					if (new File(cp_entry, package_name_slashed).isDirectory()) {
						return true;
					}
				}
			} catch (NullPointerException e) {
			} catch (IOException e) {
			}
		}
		return false;
	}

}
